iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
Modern Web

起步Go!Let's Go!系列 第 12

[ Day 12] Go 指標參數:釋放函式的潛力

  • 分享至 

  • xImage
  •  

上一章講解了什麼是指標,當學會了指標的基本操作後,接下來,要說明指標應用在哪。

函式參數傳遞

最簡單的應用在函式的參數傳遞
函式的參數傳遞就是呼叫函式時,資料如何透過參數傳遞到函式的內部,那資料是怎麼透過參數傳遞?
來看一個一般例子:

package main
import "fmt"
// 呼叫函式,傳入參數 x
func main(){
    var x int = 10
    add(x)
    fmt.Println(x)
}
// 接收參數 x,進行加法
func add(x int){
    x = x + 10
    fmt.Println("x (add) = ", x)
}

執行結果
x (add) = 20
10

先講結論,main 函式的 x 與 add 函式的 x,兩者並不相關。
首先 main 函式有一個整數變數且資料為 10,當呼叫 add 函式時,順便將 10 一起傳遞到 add 函式中,這個過程要做 Pass by Value。
當 add 函式接收到 10 的參數後,賦予一個 x 變數,將其去做 +10 的加法操作,並將 x 印出,便會是 20,並結束 add 函式。
回到 main 函式,此時的 x 並不受到剛剛 add 函式中的 x 所影響,依舊是原先的 10。
這是沒有透過指標的情況下去傳遞參數,叫做 Pass by Value。

但是你還是想要用這種方式去使 main 函式中的 x 變動,可以這樣做:

package main
import "fmt"
func main(){
    x := 10
    x = add(x)
    fmt.Println("x =", x)
}
func add(x int) int {
    x = x + 10
    fmt.Println("x (add) = ", x)
    return x
}

執行結果:
x (add) = 30
x = 30

雖然不透過指標仍然可以更改 main() 的 x,但是這種方法不是很好,因電腦的記憶體中除了有一塊區域要記得 main() 中的 x,還有一塊區域要記得 add() 中的 x,計算完後還要將 add() 的 num 再 return 給 main() 中的 x。
光看上面的步驟就很繁瑣,所以通常不會這樣寫,會用傳遞指標的方式去實現。

指標參數傳遞

如果傳遞的是指標參數呢?

package main
import "fmt"
func main(){
    var x int = 10 
    var xPtr *int = &x 
    add(xPtr)    // 傳遞指標到 add()
    fmt.Println(x) // 20
}
func add(xPtr *int){
    *xPtr = *xPtr + 10
    fmt.Println(*xPtr) // 20
}

一樣先講結論,因為傳遞的是指標,也就是記憶體位址,所以當 add 函式的操作,會連動影響到原本 main 函式。
這樣的結果很像 Ruby 中的 !,舉例:

arr.map{|itme|block} -> new_ary  # 回傳新的陣列且不影響原本的陣列
arr.map!{|itme|block} -> ary     # 回傳一個陣列且會影響原本的陣列

整個程式的步驟如下:
首先 main 函式有一個整數變數且資料為 10,隨後建立一個指標變數(xPtr),並將記憶體位址(&x)存放在其指摽變數中。
接著,呼叫 add 函式,並將 xPtr 傳遞過去,也就是把指標變數的記憶體位址傳遞到 add 函式中的參數,這個過程就要 Pass by Pointer
當記憶體位址傳遞到 add 函式後,因為要對反解過後的資料做運算,所以這邊先將記憶體位址反解過後再 +10,add 函式印出來的資料會是 20。
那反解過後的資料是什麼,也就是 main 函式中的 x,因為是複製一份記憶體位址到 add 函式中,當 add 函式的記憶體位置有變動時,兩者會有連動關係;所以當回到 main 函式中後,印出來的資料也會是 20。

練習

Pass by Poninter

package main
import "fmt"
func add(xPtr *int) {
    *xPtr = *xPtr + 10
    fmt.Println(*xPtr)
}
func main() {
    var a int = 10
    add(&a)  //不一定要給指標變數
    fmt.Println("Main Function", a)
}

會反應到原本的 main 函式。
另外一個之前有用到這個特性的功能。
和使用者要求輸入資料,運用到指標參數 Pass by Pointer:

package main
import "fmt"
func main(){
    var msg string
    // 傳遞字串變數的指標(記憶體位址)
    fmt.Scanln(&msg)
    fmt.Println(msg)
}

Scanln 內部的程式就會傳遞指標,並反解以及改變 msg 的資料,去反應在程式中,才能順利的拿到使用者輸入的資料。
當然也可以寫成這樣:

package main
import "fmt"
func main(){
    var msg string
    var msgPtr *string = &msg
    fmt.Scanln(msgPtr)
    fmt.Println(msg)
}

Slice 也是一種指標

Slice 本質也是類似一個指標,所以傳遞切片時可以直接 Slice 中的值。

package main
import "fmt"
func main(){
    s1 := []int{1, 2, 3, 4, 5}
    s2 := s1
    s2[0] = 120
    fmt.Println(s1)
    fmt.Println(s2)
}

執行結果
[120 2 3 4 5]
[120 2 3 4 5]

另一個例子:

package main
import "fmt"
func double(nums []int){
    for i := 0; i < len(nums); i++{
        nums[i] = nums[i] * 2
    }
}
func main(){
    nums := []int{1, 3, 4, 7, 9}
    double(nums)
    for _, v := range nums{
        fmt.Printf("%d", v)
    }
    fmt.Printf("%d", nums)
}

執行結果:
2 6 8 14 18

可以看到在將切片指派給新的變數時,實際上是將原始切片的指標複製一份給新的變數,所以在修改新的變數時,原始切片也會跟著被修改。

Map 也是一種指標

Map 是一種引用類型,當它作為函式參數傳遞時,會傳遞指向 Map 的指標。

package main 
import "fmt"
func plusOne(m map[string]int){
    for k, v := range m{
        m[k] = v + 100
    }
}
func main(){
    price := map[string]int{
        "apple":  20,
        "kiwi":   38,
        "melon":  69,
        "orange": 43,
	}
    plusOne(price)
    for k, v := range price{
        fmt.Printf("%s: %d\n", k, v)
    }
}

執行結果
apple: 120
kiwi: 138
melon: 169
orange: 143

Array 不是指標

在 Go 語言中,陣列是一種值類型,而不是指標類型,這意味著當你將一個陣列賦值給另一個變數時,會複製一份全新的陣列,而不是將指標指向同一個陣列。

package main
import "fmt"
func main(){
    nums := [5]int{1, 3, 4, 7, 9}
    double(nums)
    for _, v := range nums{
        fmt.Printf("%d", v)
    }
}
func double(nums [5]int){
    for i := 0; i < len(nums); i++{
        nums[i] = nums[i] * 2
    }
}

執行結果
1 3 4 7 9

雖然在函式 double() 中更改了 nums,但因為陣列 nums 傳進函式 double() 是傳送「值」而不是「址」所以並不會影響原先的陣列。

func main() {
  arr1 := [3]int{1, 2, 3}
  arr2 := arr1
  arr2[0] = 4

  fmt.Println(arr1) // [1 2 3]
  fmt.Println(arr2) // [4 2 3]
}

執行結果
[1 2 3]
[4 2 3]

宣告了一個陣列 arr1,並將其複製到了 arr2 中。當 arr2 的第一個元素改為 4 時,arr1 並沒有受到影響,因為它們是兩個獨立的陣列。這兩個例子證明了 Go 中的陣列不是指標。

陣列利用取址也可傳遞指標

如果你想透過函式更改陣列的值,可以透過取址的方式將其實現。

package main
import "fmt"
func double(nums *[5]int){
    for i := 0; i < len(nums); i = i+1{
        (*nums)[i] = (*nums)[i] * 2
    }
}

func main(){
    nums := [5]int{1, 3, 4, 7, 9}
    double(&nums)
    for _, v := range nums{
        fmt.Printf("%d ", v)
    }
}

執行結果
2 6 8 14 18

參考資料:

  1. Golang 指標參數 - Pass by Pointer 和 Pass by Value 函式參數傳遞 By 彭彭

上一篇
[ Day 11] Go 指標與記憶體魔法
下一篇
[ Day 13] Go 結構魔法:定義、實體化、編織
系列文
起步Go!Let's Go!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言